package org.apache.lucene.index;

/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;

import java.util.List;
import java.util.Map;
import java.util.Set;

import java.util.concurrent.atomic.AtomicInteger;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.FieldSelector;
import org.apache.lucene.search.Similarity;
import org.apache.lucene.index.FieldInfo.IndexOptions;
import org.apache.lucene.store.BufferedIndexInput;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.IndexInput;
import org.apache.lucene.util.BitVector;
import org.apache.lucene.util.CloseableThreadLocal;
import org.apache.lucene.util.StringHelper;

/**
 * @lucene.experimental
 */
public class SegmentReader extends IndexReader implements Cloneable {
    protected boolean readOnly;

    private SegmentInfo si;
    private int readBufferSize;

    CloseableThreadLocal<FieldsReader> fieldsReaderLocal = new FieldsReaderLocal();
    CloseableThreadLocal<TermVectorsReader> termVectorsLocal = new CloseableThreadLocal<TermVectorsReader>();

    BitVector deletedDocs = null;
    AtomicInteger deletedDocsRef = null;
    private boolean deletedDocsDirty = false;
    private boolean normsDirty = false;

    // TODO: we should move this tracking into SegmentInfo;
    // this way SegmentInfo.toString shows pending deletes
    private int pendingDeleteCount;

    private boolean rollbackHasChanges = false;
    private boolean rollbackDeletedDocsDirty = false;
    private boolean rollbackNormsDirty = false;
    private SegmentInfo rollbackSegmentInfo;
    private int rollbackPendingDeleteCount;

    // optionally used for the .nrm file shared by multiple norms
    IndexInput singleNormStream;
    AtomicInteger singleNormRef;

    SegmentCoreReaders core;

    /**
     * Sets the initial value 
     */
    private class FieldsReaderLocal extends CloseableThreadLocal<FieldsReader> {
        @Override
        protected FieldsReader initialValue() {
            return (FieldsReader) core.getFieldsReaderOrig().clone();
        }
    }

    Map<String, SegmentNorms> norms = new HashMap<String, SegmentNorms>();

    /**
     * @throws CorruptIndexException if the index is corrupt
     * @throws IOException if there is a low-level IO error
     */
    public static SegmentReader get(boolean readOnly, SegmentInfo si, int termInfosIndexDivisor) throws CorruptIndexException, IOException {
        return get(readOnly, si.dir, si, BufferedIndexInput.BUFFER_SIZE, true, termInfosIndexDivisor);
    }

    /**
     * @throws CorruptIndexException if the index is corrupt
     * @throws IOException if there is a low-level IO error
     */
    public static SegmentReader get(boolean readOnly, Directory dir, SegmentInfo si, int readBufferSize, boolean doOpenStores, int termInfosIndexDivisor) throws CorruptIndexException, IOException {
        SegmentReader instance = readOnly ? new ReadOnlySegmentReader() : new SegmentReader();
        instance.readOnly = readOnly;
        instance.si = si;
        instance.readBufferSize = readBufferSize;

        boolean success = false;

        try {
            instance.core = new SegmentCoreReaders(instance, dir, si, readBufferSize, termInfosIndexDivisor);
            if (doOpenStores) {
                instance.core.openDocStores(si);
            }
            instance.loadDeletedDocs();
            instance.openNorms(instance.core.cfsDir, readBufferSize);
            success = true;
        } finally {

            // With lock-less commits, it's entirely possible (and
            // fine) to hit a FileNotFound exception above.  In
            // this case, we want to explicitly close any subset
            // of things that were opened so that we don't have to
            // wait for a GC to do so.
            if (!success) {
                instance.doClose();
            }
        }
        return instance;
    }

    void openDocStores() throws IOException {
        core.openDocStores(si);
    }

    private boolean checkDeletedCounts() throws IOException {
        final int recomputedCount = deletedDocs.getRecomputedCount();

        assert deletedDocs.count() == recomputedCount : "deleted count=" + deletedDocs.count() + " vs recomputed count=" + recomputedCount;

        assert si.getDelCount() == recomputedCount : "delete count mismatch: info=" + si.getDelCount() + " vs BitVector=" + recomputedCount;

        // Verify # deletes does not exceed maxDoc for this
        // segment:
        assert si.getDelCount() <= maxDoc() : "delete count mismatch: " + recomputedCount + ") exceeds max doc (" + maxDoc() + ") for segment " + si.name;

        return true;
    }

    private void loadDeletedDocs() throws IOException {
        // NOTE: the bitvector is stored using the regular directory, not cfs
        if (hasDeletions(si)) {
            deletedDocs = new BitVector(directory(), si.getDelFileName());
            deletedDocsRef = new AtomicInteger(1);
            assert checkDeletedCounts();
            if (deletedDocs.size() != si.docCount) {
                throw new CorruptIndexException("document count mismatch: deleted docs count " + deletedDocs.size() + " vs segment doc count " + si.docCount + " segment=" + si.name);
            }
        } else
            assert si.getDelCount() == 0;
    }

    /**
     * Clones the norm bytes.  May be overridden by subclasses.  New and experimental.
     * @param bytes Byte array to clone
     * @return New BitVector
     */
    protected byte[] cloneNormBytes(byte[] bytes) {
        byte[] cloneBytes = new byte[bytes.length];
        System.arraycopy(bytes, 0, cloneBytes, 0, bytes.length);
        return cloneBytes;
    }

    /**
     * Clones the deleteDocs BitVector.  May be overridden by subclasses. New and experimental.
     * @param bv BitVector to clone
     * @return New BitVector
     */
    protected BitVector cloneDeletedDocs(BitVector bv) {
        ensureOpen();
        return (BitVector) bv.clone();
    }

    @Override
    public final synchronized Object clone() {
        try {
            return clone(readOnly); // Preserve current readOnly
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
    }

    @Override
    public final synchronized IndexReader clone(boolean openReadOnly) throws CorruptIndexException, IOException {
        return reopenSegment(si, true, openReadOnly);
    }

    @Override
    protected synchronized IndexReader doOpenIfChanged() throws CorruptIndexException, IOException {
        return reopenSegment(si, false, readOnly);
    }

    @Override
    protected synchronized IndexReader doOpenIfChanged(boolean openReadOnly) throws CorruptIndexException, IOException {
        return reopenSegment(si, false, openReadOnly);
    }

    synchronized SegmentReader reopenSegment(SegmentInfo si, boolean doClone, boolean openReadOnly) throws CorruptIndexException, IOException {
        ensureOpen();
        boolean deletionsUpToDate = (this.si.hasDeletions() == si.hasDeletions()) && (!si.hasDeletions() || this.si.getDelFileName().equals(si.getDelFileName()));
        boolean normsUpToDate = true;

        boolean[] fieldNormsChanged = new boolean[core.fieldInfos.size()];
        final int fieldCount = core.fieldInfos.size();
        for (int i = 0; i < fieldCount; i++) {
            if (!this.si.getNormFileName(i).equals(si.getNormFileName(i))) {
                normsUpToDate = false;
                fieldNormsChanged[i] = true;
            }
        }

        // if we're cloning we need to run through the reopenSegment logic
        // also if both old and new readers aren't readonly, we clone to avoid sharing modifications
        if (normsUpToDate && deletionsUpToDate && !doClone && openReadOnly && readOnly) {
            return null;
        }

        // When cloning, the incoming SegmentInfos should not
        // have any changes in it:
        assert !doClone || (normsUpToDate && deletionsUpToDate);

        // clone reader
        SegmentReader clone = openReadOnly ? new ReadOnlySegmentReader() : new SegmentReader();

        boolean success = false;
        try {
            core.incRef();
            clone.core = core;
            clone.readOnly = openReadOnly;
            clone.si = si;
            clone.readBufferSize = readBufferSize;
            clone.pendingDeleteCount = pendingDeleteCount;
            clone.readerFinishedListeners = readerFinishedListeners;

            if (!openReadOnly && hasChanges) {
                // My pending changes transfer to the new reader
                clone.deletedDocsDirty = deletedDocsDirty;
                clone.normsDirty = normsDirty;
                clone.hasChanges = hasChanges;
                hasChanges = false;
            }

            if (doClone) {
                if (deletedDocs != null) {
                    deletedDocsRef.incrementAndGet();
                    clone.deletedDocs = deletedDocs;
                    clone.deletedDocsRef = deletedDocsRef;
                }
            } else {
                if (!deletionsUpToDate) {
                    // load deleted docs
                    assert clone.deletedDocs == null;
                    clone.loadDeletedDocs();
                } else if (deletedDocs != null) {
                    deletedDocsRef.incrementAndGet();
                    clone.deletedDocs = deletedDocs;
                    clone.deletedDocsRef = deletedDocsRef;
                }
            }

            clone.norms = new HashMap<String, SegmentNorms>();

            // Clone norms
            for (int i = 0; i < fieldNormsChanged.length; i++) {

                // Clone unchanged norms to the cloned reader
                if (doClone || !fieldNormsChanged[i]) {
                    final String curField = core.fieldInfos.fieldInfo(i).name;
                    SegmentNorms norm = this.norms.get(curField);
                    if (norm != null)
                        clone.norms.put(curField, (SegmentNorms) norm.clone());
                }
            }

            // If we are not cloning, then this will open anew
            // any norms that have changed:
            clone.openNorms(si.getUseCompoundFile() ? core.getCFSReader() : directory(), readBufferSize);

            success = true;
        } finally {
            if (!success) {
                // An exception occurred during reopen, we have to decRef the norms
                // that we incRef'ed already and close singleNormsStream and FieldsReader
                clone.decRef();
            }
        }

        return clone;
    }

    @Override
    protected void doCommit(Map<String, String> commitUserData) throws IOException {
        if (hasChanges) {
            startCommit();
            boolean success = false;
            try {
                commitChanges(commitUserData);
                success = true;
            } finally {
                if (!success) {
                    rollbackCommit();
                }
            }
        }
    }

    private synchronized void commitChanges(Map<String, String> commitUserData) throws IOException {
        if (deletedDocsDirty) { // re-write deleted
            si.advanceDelGen();

            assert deletedDocs.size() == si.docCount;

            // We can write directly to the actual name (vs to a
            // .tmp & renaming it) because the file is not live
            // until segments file is written:
            final String delFileName = si.getDelFileName();
            boolean success = false;
            try {
                deletedDocs.write(directory(), delFileName);
                success = true;
            } finally {
                if (!success) {
                    try {
                        directory().deleteFile(delFileName);
                    } catch (Throwable t) {
                        // suppress this so we keep throwing the
                        // original exception
                    }
                }
            }

            si.setDelCount(si.getDelCount() + pendingDeleteCount);
            pendingDeleteCount = 0;
            assert deletedDocs.count() == si.getDelCount() : "delete count mismatch during commit: info=" + si.getDelCount() + " vs BitVector=" + deletedDocs.count();
        } else {
            assert pendingDeleteCount == 0;
        }

        if (normsDirty) { // re-write norms
            si.setNumFields(core.fieldInfos.size());
            for (final SegmentNorms norm : norms.values()) {
                if (norm.dirty) {
                    norm.reWrite(si);
                }
            }
        }
        deletedDocsDirty = false;
        normsDirty = false;
        hasChanges = false;
    }

    FieldsReader getFieldsReader() {
        return fieldsReaderLocal.get();
    }

    @Override
    protected void doClose() throws IOException {
        termVectorsLocal.close();
        fieldsReaderLocal.close();

        if (deletedDocs != null) {
            deletedDocsRef.decrementAndGet();
            // null so if an app hangs on to us we still free most ram
            deletedDocs = null;
        }

        for (final SegmentNorms norm : norms.values()) {
            norm.decRef();
        }
        if (core != null) {
            core.decRef();
        }
    }

    static boolean hasDeletions(SegmentInfo si) throws IOException {
        // Don't call ensureOpen() here (it could affect performance)
        return si.hasDeletions();
    }

    @Override
    public boolean hasDeletions() {
        // Don't call ensureOpen() here (it could affect performance)
        return deletedDocs != null;
    }

    static boolean usesCompoundFile(SegmentInfo si) throws IOException {
        return si.getUseCompoundFile();
    }

    static boolean hasSeparateNorms(SegmentInfo si) throws IOException {
        return si.hasSeparateNorms();
    }

    @Override
    protected void doDelete(int docNum) {
        if (deletedDocs == null) {
            deletedDocs = new BitVector(maxDoc());
            deletedDocsRef = new AtomicInteger(1);
        }
        // there is more than 1 SegmentReader with a reference to this
        // deletedDocs BitVector so decRef the current deletedDocsRef,
        // clone the BitVector, create a new deletedDocsRef
        if (deletedDocsRef.get() > 1) {
            AtomicInteger oldRef = deletedDocsRef;
            deletedDocs = cloneDeletedDocs(deletedDocs);
            deletedDocsRef = new AtomicInteger(1);
            oldRef.decrementAndGet();
        }
        deletedDocsDirty = true;
        if (!deletedDocs.getAndSet(docNum)) {
            pendingDeleteCount++;
        }
    }

    @Override
    protected void doUndeleteAll() {
        deletedDocsDirty = false;
        if (deletedDocs != null) {
            assert deletedDocsRef != null;
            deletedDocsRef.decrementAndGet();
            deletedDocs = null;
            deletedDocsRef = null;
            pendingDeleteCount = 0;
            si.clearDelGen();
            si.setDelCount(0);
        } else {
            assert deletedDocsRef == null;
            assert pendingDeleteCount == 0;
        }
    }

    List<String> files() throws IOException {
        return new ArrayList<String>(si.files());
    }

    @Override
    public TermEnum terms() {
        ensureOpen();
        return core.getTermsReader().terms();
    }

    @Override
    public TermEnum terms(Term t) throws IOException {
        ensureOpen();
        return core.getTermsReader().terms(t);
    }

    FieldInfos fieldInfos() {
        return core.fieldInfos;
    }

    @Override
    public Document document(int n, FieldSelector fieldSelector) throws CorruptIndexException, IOException {
        ensureOpen();
        if (n < 0 || n >= maxDoc()) {
            throw new IllegalArgumentException("docID must be >= 0 and < maxDoc=" + maxDoc() + " (got docID=" + n + ")");
        }
        return getFieldsReader().doc(n, fieldSelector);
    }

    @Override
    public synchronized boolean isDeleted(int n) {
        return (deletedDocs != null && deletedDocs.get(n));
    }

    @Override
    public TermDocs termDocs(Term term) throws IOException {
        if (term == null) {
            return new AllTermDocs(this);
        } else {
            return super.termDocs(term);
        }
    }

    @Override
    public TermDocs termDocs() throws IOException {
        ensureOpen();
        return new SegmentTermDocs(this);
    }

    @Override
    public TermPositions termPositions() throws IOException {
        ensureOpen();
        return new SegmentTermPositions(this);
    }

    @Override
    public int docFreq(Term t) throws IOException {
        ensureOpen();
        TermInfo ti = core.getTermsReader().get(t);
        if (ti != null)
            return ti.docFreq;
        else
            return 0;
    }

    @Override
    public int numDocs() {
        // Don't call ensureOpen() here (it could affect performance)
        int n = maxDoc();
        if (deletedDocs != null)
            n -= deletedDocs.count();
        return n;
    }

    @Override
    public int maxDoc() {
        // Don't call ensureOpen() here (it could affect performance)
        return si.docCount;
    }

    /**
     * @see IndexReader#getFieldNames(org.apache.lucene.index.IndexReader.FieldOption)
     */
    @Override
    public Collection<String> getFieldNames(IndexReader.FieldOption fieldOption) {
        ensureOpen();

        Set<String> fieldSet = new HashSet<String>();
        for (int i = 0; i < core.fieldInfos.size(); i++) {
            FieldInfo fi = core.fieldInfos.fieldInfo(i);
            if (fieldOption == IndexReader.FieldOption.ALL) {
                fieldSet.add(fi.name);
            } else if (!fi.isIndexed && fieldOption == IndexReader.FieldOption.UNINDEXED) {
                fieldSet.add(fi.name);
            } else if (fi.indexOptions == IndexOptions.DOCS_ONLY && fieldOption == IndexReader.FieldOption.OMIT_TERM_FREQ_AND_POSITIONS) {
                fieldSet.add(fi.name);
            } else if (fi.indexOptions == IndexOptions.DOCS_AND_FREQS && fieldOption == IndexReader.FieldOption.OMIT_POSITIONS) {
                fieldSet.add(fi.name);
            } else if (fi.storePayloads && fieldOption == IndexReader.FieldOption.STORES_PAYLOADS) {
                fieldSet.add(fi.name);
            } else if (fi.isIndexed && fieldOption == IndexReader.FieldOption.INDEXED) {
                fieldSet.add(fi.name);
            } else if (fi.isIndexed && fi.storeTermVector == false && fieldOption == IndexReader.FieldOption.INDEXED_NO_TERMVECTOR) {
                fieldSet.add(fi.name);
            } else if (fi.storeTermVector == true && fi.storePositionWithTermVector == false && fi.storeOffsetWithTermVector == false && fieldOption == IndexReader.FieldOption.TERMVECTOR) {
                fieldSet.add(fi.name);
            } else if (fi.isIndexed && fi.storeTermVector && fieldOption == IndexReader.FieldOption.INDEXED_WITH_TERMVECTOR) {
                fieldSet.add(fi.name);
            } else if (fi.storePositionWithTermVector && fi.storeOffsetWithTermVector == false && fieldOption == IndexReader.FieldOption.TERMVECTOR_WITH_POSITION) {
                fieldSet.add(fi.name);
            } else if (fi.storeOffsetWithTermVector && fi.storePositionWithTermVector == false && fieldOption == IndexReader.FieldOption.TERMVECTOR_WITH_OFFSET) {
                fieldSet.add(fi.name);
            } else if ((fi.storeOffsetWithTermVector && fi.storePositionWithTermVector) && fieldOption == IndexReader.FieldOption.TERMVECTOR_WITH_POSITION_OFFSET) {
                fieldSet.add(fi.name);
            }
        }
        return fieldSet;
    }

    @Override
    public boolean hasNorms(String field) {
        ensureOpen();
        return norms.containsKey(field);
    }

    @Override
    public byte[] norms(String field) throws IOException {
        ensureOpen();
        final SegmentNorms norm = norms.get(field);
        if (norm == null) {
            // not indexed, or norms not stored
            return null;
        }
        return norm.bytes();
    }

    @Override
    protected void doSetNorm(int doc, String field, byte value) throws IOException {
        SegmentNorms norm = norms.get(field);
        if (norm == null) {
            // field does not store norms
            throw new IllegalStateException("Cannot setNorm for field " + field + ": norms were omitted");
        }

        normsDirty = true;
        norm.copyOnWrite()[doc] = value; // set the value
    }

    /** Read norms into a pre-allocated array. */
    @Override
    public synchronized void norms(String field, byte[] bytes, int offset) throws IOException {

        ensureOpen();
        SegmentNorms norm = norms.get(field);
        if (norm == null) {
            Arrays.fill(bytes, offset, bytes.length, Similarity.getDefault().encodeNormValue(1.0f));
            return;
        }

        norm.bytes(bytes, offset, maxDoc());
    }

    // For testing
    /** @lucene.internal */
    int getPostingsSkipInterval() {
        return core.getTermsReader().getSkipInterval();
    }

    private void openNorms(Directory cfsDir, int readBufferSize) throws IOException {
        long nextNormSeek = SegmentNorms.NORMS_HEADER.length; //skip header (header unused for now)
        int maxDoc = maxDoc();
        for (int i = 0; i < core.fieldInfos.size(); i++) {
            FieldInfo fi = core.fieldInfos.fieldInfo(i);
            if (norms.containsKey(fi.name)) {
                // in case this SegmentReader is being re-opened, we might be able to
                // reuse some norm instances and skip loading them here
                continue;
            }
            if (fi.isIndexed && !fi.omitNorms) {
                Directory d = directory();
                String fileName = si.getNormFileName(fi.number);
                if (!si.hasSeparateNorms(fi.number)) {
                    d = cfsDir;
                }

                // singleNormFile means multiple norms share this file
                boolean singleNormFile = IndexFileNames.matchesExtension(fileName, IndexFileNames.NORMS_EXTENSION);
                IndexInput normInput = null;
                long normSeek;

                if (singleNormFile) {
                    normSeek = nextNormSeek;
                    if (singleNormStream == null) {
                        singleNormStream = d.openInput(fileName, readBufferSize);
                        singleNormRef = new AtomicInteger(1);
                    } else {
                        singleNormRef.incrementAndGet();
                    }
                    // All norms in the .nrm file can share a single IndexInput since
                    // they are only used in a synchronized context.
                    // If this were to change in the future, a clone could be done here.
                    normInput = singleNormStream;
                } else {
                    normInput = d.openInput(fileName);
                    // if the segment was created in 3.2 or after, we wrote the header for sure,
                    // and don't need to do the sketchy file size check. otherwise, we check 
                    // if the size is exactly equal to maxDoc to detect a headerless file.
                    // NOTE: remove this check in Lucene 5.0!
                    String version = si.getVersion();
                    final boolean isUnversioned = (version == null || StringHelper.getVersionComparator().compare(version, "3.2") < 0) && normInput.length() == maxDoc();
                    if (isUnversioned) {
                        normSeek = 0;
                    } else {
                        normSeek = SegmentNorms.NORMS_HEADER.length;
                    }
                }

                norms.put(fi.name, new SegmentNorms(normInput, fi.number, normSeek, this));
                nextNormSeek += maxDoc; // increment also if some norms are separate
            }
        }
    }

    boolean termsIndexLoaded() {
        return core.termsIndexIsLoaded();
    }

    // NOTE: only called from IndexWriter when a near
    // real-time reader is opened, or applyDeletes is run,
    // sharing a segment that's still being merged.  This
    // method is not thread safe, and relies on the
    // synchronization in IndexWriter
    void loadTermsIndex(int termsIndexDivisor) throws IOException {
        core.loadTermsIndex(si, termsIndexDivisor);
    }

    // for testing only
    boolean normsClosed() {
        if (singleNormStream != null) {
            return false;
        }
        for (final SegmentNorms norm : norms.values()) {
            if (norm.refCount > 0) {
                return false;
            }
        }
        return true;
    }

    // for testing only
    boolean normsClosed(String field) {
        return norms.get(field).refCount == 0;
    }

    /**
     * Create a clone from the initial TermVectorsReader and store it in the ThreadLocal.
     * @return TermVectorsReader
     */
    TermVectorsReader getTermVectorsReader() {
        TermVectorsReader tvReader = termVectorsLocal.get();
        if (tvReader == null) {
            TermVectorsReader orig = core.getTermVectorsReaderOrig();
            if (orig == null) {
                return null;
            } else {
                try {
                    tvReader = (TermVectorsReader) orig.clone();
                } catch (CloneNotSupportedException cnse) {
                    return null;
                }
            }
            termVectorsLocal.set(tvReader);
        }
        return tvReader;
    }

    TermVectorsReader getTermVectorsReaderOrig() {
        return core.getTermVectorsReaderOrig();
    }

    /** Return a term frequency vector for the specified document and field. The
     *  vector returned contains term numbers and frequencies for all terms in
     *  the specified field of this document, if the field had storeTermVector
     *  flag set.  If the flag was not set, the method returns null.
     * @throws IOException
     */
    @Override
    public TermFreqVector getTermFreqVector(int docNumber, String field) throws IOException {
        // Check if this field is invalid or has no stored term vector
        ensureOpen();
        FieldInfo fi = core.fieldInfos.fieldInfo(field);
        if (fi == null || !fi.storeTermVector)
            return null;

        TermVectorsReader termVectorsReader = getTermVectorsReader();
        if (termVectorsReader == null)
            return null;

        return termVectorsReader.get(docNumber, field);
    }

    @Override
    public void getTermFreqVector(int docNumber, String field, TermVectorMapper mapper) throws IOException {
        ensureOpen();
        FieldInfo fi = core.fieldInfos.fieldInfo(field);
        if (fi == null || !fi.storeTermVector)
            return;

        TermVectorsReader termVectorsReader = getTermVectorsReader();
        if (termVectorsReader == null) {
            return;
        }

        termVectorsReader.get(docNumber, field, mapper);
    }

    @Override
    public void getTermFreqVector(int docNumber, TermVectorMapper mapper) throws IOException {
        ensureOpen();

        TermVectorsReader termVectorsReader = getTermVectorsReader();
        if (termVectorsReader == null)
            return;

        termVectorsReader.get(docNumber, mapper);
    }

    /** Return an array of term frequency vectors for the specified document.
     *  The array contains a vector for each vectorized field in the document.
     *  Each vector vector contains term numbers and frequencies for all terms
     *  in a given vectorized field.
     *  If no such fields existed, the method returns null.
     * @throws IOException
     */
    @Override
    public TermFreqVector[] getTermFreqVectors(int docNumber) throws IOException {
        ensureOpen();

        TermVectorsReader termVectorsReader = getTermVectorsReader();
        if (termVectorsReader == null)
            return null;

        return termVectorsReader.get(docNumber);
    }

    /** {@inheritDoc} */
    @Override
    public String toString() {
        final StringBuilder buffer = new StringBuilder();
        if (hasChanges) {
            buffer.append('*');
        }
        buffer.append(si.toString(core.dir, pendingDeleteCount));
        return buffer.toString();
    }

    /**
     * Return the name of the segment this reader is reading.
     */
    public String getSegmentName() {
        return core.segment;
    }

    /**
     * Return the SegmentInfo of the segment this reader is reading.
     */
    SegmentInfo getSegmentInfo() {
        return si;
    }

    void setSegmentInfo(SegmentInfo info) {
        si = info;
    }

    void startCommit() {
        rollbackSegmentInfo = (SegmentInfo) si.clone();
        rollbackHasChanges = hasChanges;
        rollbackDeletedDocsDirty = deletedDocsDirty;
        rollbackNormsDirty = normsDirty;
        rollbackPendingDeleteCount = pendingDeleteCount;
        for (SegmentNorms norm : norms.values()) {
            norm.rollbackDirty = norm.dirty;
        }
    }

    void rollbackCommit() {
        si.reset(rollbackSegmentInfo);
        hasChanges = rollbackHasChanges;
        deletedDocsDirty = rollbackDeletedDocsDirty;
        normsDirty = rollbackNormsDirty;
        pendingDeleteCount = rollbackPendingDeleteCount;
        for (SegmentNorms norm : norms.values()) {
            norm.dirty = norm.rollbackDirty;
        }
    }

    /** Returns the directory this index resides in. */
    @Override
    public Directory directory() {
        // Don't ensureOpen here -- in certain cases, when a
        // cloned/reopened reader needs to commit, it may call
        // this method on the closed original reader
        return core.dir;
    }

    // This is necessary so that cloned SegmentReaders (which
    // share the underlying postings data) will map to the
    // same entry in the FieldCache.  See LUCENE-1579.
    @Override
    public final Object getCoreCacheKey() {
        return core.freqStream;
    }

    @Override
    public Object getDeletesCacheKey() {
        return deletedDocs;
    }

    @Override
    public long getUniqueTermCount() {
        return core.getTermsReader().size();
    }

    /**
     * Lotsa tests did hacks like:<br/>
     * SegmentReader reader = (SegmentReader) IndexReader.open(dir);<br/>
     * They broke. This method serves as a hack to keep hacks working
     * We do it with R/W access for the tests (BW compatibility)
     * @deprecated Remove this when tests are fixed!
     */
    @Deprecated
    static SegmentReader getOnlySegmentReader(Directory dir) throws IOException {
        return getOnlySegmentReader(IndexReader.open(dir, false));
    }

    static SegmentReader getOnlySegmentReader(IndexReader reader) {
        if (reader instanceof SegmentReader)
            return (SegmentReader) reader;

        if (reader instanceof DirectoryReader) {
            IndexReader[] subReaders = reader.getSequentialSubReaders();
            if (subReaders.length != 1)
                throw new IllegalArgumentException(reader + " has " + subReaders.length + " segments instead of exactly one");

            return (SegmentReader) subReaders[0];
        }

        throw new IllegalArgumentException(reader + " is not a SegmentReader or a single-segment DirectoryReader");
    }

    @Override
    public int getTermInfosIndexDivisor() {
        return core.termsIndexDivisor;
    }

    @Override
    protected void readerFinished() {
        // Do nothing here -- we have more careful control on
        // when to notify that a SegmentReader has finished,
        // because a given core is shared across many cloned
        // SegmentReaders.  We only notify once that core is no
        // longer used (all SegmentReaders sharing it have been
        // closed).
    }
}
